//License: Artistic License v2.0, http://opensource.org/licenses/Artistic-2.0

/*
	Gobl is a CLI-driven CMS using Go standard HTTP server.
	During start up (or update) some things are statically pre-assembled
	/ pre-generated and stored in memory, but the final HTML is generated
	only at runtime by request.  Everything is in memory (except resource
	files like CSS, JS, IMG, etc).

	Conceptually, you can run gobl simultaneously on three machines.

		- The web server:       go run gobl.go -r=ws
		- The web log server:   go run gobl.go -r=logger
		- CLI for admin tasks:  go run gobl.go -r=update

	The web GUI maintenance client was added so GUI-only folks can use it.

		- Instead of -r=ws:     go run gobl.go -r=cms

	Pre-requisites:

		- File folders exists in data: configs, copies, snippets, log, files.
		- Config file must be found and properly set, e.g. in data/configs/config.json.
*/
package main

import (
	"gobl/misc"
	"encoding/base64"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"
	"text/template"
	"time"
)

//some maps must be initialised or else panic on empty lookup e.g. readSnippets
var supportedLang = map[string]bool{}
var sessions = map[string]Session{}
var copies = map[string]Copy{}
var snippets = map[string](map[string]string){}
var users map[string]User
var conf Configuration
var blogIndex []string
var confJSON = "data/configs/config.json"
var rFlag *string = flag.String("r", "", "ws / cms / update / logger")

func main() {
	flag.Parse()
	var err error
	conf, err = loadConfigFile(confJSON)
	if err != nil {
		panic(err)
	}
	switch *rFlag {
	case "ws":
		startWebServer()
	case "cms":
		startWebServer()
	case "update":
		sendRPC("update")
	case "logger":
		startHTTPLogServer()
	default:
		log.Println("Please use -h to see valid options.")
	}
}

func sendRPC(arg string) {
	if conf.ServeRPC {
		misc.CallRPC(conf.RPCConnType, conf.RPCConnAddr, "Admin.Do", arg)
		os.Exit(0)
	}
}

func startHTTPLogServer() {
	if conf.ServeHTTPLogging {
		misc.SetInboundChannel(conf.LogChannelName, conf.LogChannelType, conf.LogChannelAddr, conf.LogChannelBufSize, 3)
		misc.InboundChannelToFile(conf.LogPath, "hlog", "csv", conf.LogChannelBufSize, true)
		os.Exit(0)
	}
}

func startWebServer() {
	if conf.ServeRPC {
		go misc.ListenAndServeRPC(new(Admin), conf.RPCConnType, conf.RPCConnAddr)
	}
	if conf.ServeHTTPLogging {
		err := misc.SetOutboundChannel(conf.LogChannelName, conf.LogChannelType, conf.LogChannelAddr)
		if err != nil {
			panic(err)
		}
	}
	fname := fmt.Sprintf("%sslog%s.log",
		conf.LogPath, strconv.FormatInt(time.Now().Unix(), 10))
	SlogFile, err := os.OpenFile(
		fname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
	defer SlogFile.Close()
	if err != nil {
		log.Println(err) //to std out
	}
	log.SetOutput(SlogFile) //log file name == server start time
	err = loadDataFiles()
	if err != nil {
		log.Println("Init / restore failed")
		panic(err)
	}
	if *rFlag == "cms" {
		http.HandleFunc("/getcopy/", getcopy)
		http.HandleFunc("/postcopy/", postcopy)
		http.HandleFunc("/login/", login)
		http.HandleFunc("/logout/", logout)
		http.HandleFunc("/postlogin/", postlogin)
		http.HandleFunc("/getuser/", getuser)
		http.HandleFunc("/postuser/", postuser)
		http.HandleFunc("/getsnippet/", getsnippet)
		http.HandleFunc("/postsnippet/", postsnippet)
		http.HandleFunc("/getsite/", getsite)
	}
	http.HandleFunc("/search/", search)
	http.HandleFunc("/favicon.ico", servefile)
	http.HandleFunc("/sitemap.txt", servefile)
	http.HandleFunc("/robots.txt", servefile)
	http.HandleFunc("/img/", servefile)
	http.HandleFunc("/css/", servefile)
	http.HandleFunc("/js/", servefile)
	http.HandleFunc("/download/", servedownload)
	http.HandleFunc("/blog/", getblog)
	http.HandleFunc("/", getpage)
	if conf.ServeTLS {
		http.ListenAndServeTLS(conf.TLSServerPort, conf.TLSCertFile,
			conf.TLSKeyFile, nil)
	} else {
		http.ListenAndServe(conf.HTTPServerPort, nil)
	}
}

func servefile(w http.ResponseWriter, r *http.Request) {
	file := r.URL.Path
	http.ServeFile(w, r, conf.FilePath+file)
}

func servedownload(w http.ResponseWriter, r *http.Request) {
	file := r.URL.Path
	if conf.ServeHTTPLogging {
		go logHTTPRequest(r)
	}
	http.ServeFile(w, r, conf.FilePath+file)
}

func search(w http.ResponseWriter, r *http.Request) {
	//assumes domain/search/lang & lang is set in search form
	u := setUserRequest(r)
	u.setLang(getStr(u.Args, 2))
	var p, templateName string
	if misc.IsMobile(r.UserAgent()) {
		templateName = "page4mobile"
	} else {
		templateName = "page"
	}
	p = readSnippet(u.Lang, templateName)
	p = strings.Replace(p, "{Title}", readSnippet(u.Lang, "SiteSearchTitle"), -1)
	str := strings.TrimSpace(r.FormValue("search"))
	if len(str) < 2 { //CN,KR,JP: 2 chars == 2 words
		p = strings.Replace(p, "<!--Copy-->", "", -1) //results == ""
		fmt.Fprintf(w, p)
		return
	}
	var results, tmp []string
	for _, c := range copies { //brute searching + scoring
		score := c.search(str) //used for sorting purpose
		if score > 0 {
			url := fmt.Sprintf("%012d<p><a href=\"/%s/\">%s</a></p>", score, c.Cid, c.Title)
			tmp = append(tmp, url)
		}
	}
	sort.Strings(tmp) //std lib does not have descending sort?
	for i := len(tmp) - 1; i >= 0; i-- {
		results = append(results, tmp[i][12:]) //drop the score from output
	}
	p = strings.Replace(p, "<!--Copy-->", misc.SliceToString(results), -1)
	fmt.Fprintf(w, p)
	if conf.ServeHTTPLogging {
		go logHTTPRequest(r)
	}
}

func postcopy(w http.ResponseWriter, r *http.Request) {
	var u UserRequest
	var session Session
	u, session = getLoginSession(r)
	if session.Id == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	session.resetMsg()
	var c Copy
	c.Cid = strings.ToLower(strings.TrimSpace(r.FormValue("cid")))
	//TODO: needs to ensure  only [a-z0-9] and '-' is in c.Cid
	c.Copy = r.FormValue("copy")
	c.SEOTags = r.FormValue("seotags")
	c.Title = r.FormValue("title")
	c.Lang = r.FormValue("copylang")
	c.Date = r.FormValue("copydate") //TODO: input validation
	if r.FormValue("layout") == "true" {
		c.UsesOwnLayout = true
	} else {
		c.UsesOwnLayout = false
	}
	c.save()
	setSiteMap()
	setBlogIndex()
	err := backupCopies()
	if err != nil {
		log.Println(err)
	}
	http.Redirect(w, r, "/getsite/"+u.Lang+"/"+session.Id,
		http.StatusFound)
}

func getcopy(w http.ResponseWriter, r *http.Request) {
	var u UserRequest
	var session Session
	u, session = getLoginSession(r)
	if session.Id == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	var c Copy
	var cid string
	if len(u.Args) >= 5 { //edit or create mode
		cid = getStr(u.Args, 4)
		c = readCopy(cid)
	}
	var msg []string
	if session.Last == "getcopy" {
		msg = session.LastMsg
	}
	var langButtons, layoutButtons, t string
	for lang := range supportedLang {
		if lang == c.Lang || (c.Lang == "" && lang == conf.DefaultLang) {
			t = fmt.Sprintf(
				"<input type=\"radio\" name=\"copylang\" value=\"%s\" checked>%s</input>\n",
				lang, lang)
		} else {
			t = fmt.Sprintf(
				"<input type=\"radio\" name=\"copylang\" value=\"%s\">%s</input>\n",
				lang, lang)
		}
		langButtons = langButtons + t
	}
	if c.UsesOwnLayout {
		layoutButtons = "<input type=\"radio\" name=\"layout\" value=\"true\" checked>true</input>\n" +
			"<input type=\"radio\" name=\"layout\" value=\"false\">false</input>\n"
	} else {
		layoutButtons = "<input type=\"radio\" name=\"layout\" value=\"true\">true</input>\n" +
			"<input type=\"radio\" name=\"layout\" value=\"false\" checked>false</input>\n"
	}
	page := readSnippet(u.Lang, "cmsLayout")
	page = strings.Replace(page, "<!--Copy-->", readSnippet(u.Lang, "getcopy"), -1)
	page = strings.Replace(page, "<!--cmsNav-->", readSnippet(u.Lang, "cmsNav"), -1)
	page = strings.Replace(page, "<!--Msg-->", readSnippets(u.Lang, msg), -1)
	page = strings.Replace(page, "<!--langButtons-->", langButtons, -1)
	page = strings.Replace(page, "<!--layoutButtons-->", layoutButtons, -1)
	page = strings.Replace(page, "{Lang}", u.Lang, -1)
	page = strings.Replace(page, "{Title}", c.Title, -1)
	page = strings.Replace(page, "{Sid}", session.Id, -1)
	page = strings.Replace(page, "{SEOTags}", c.SEOTags, -1)
	page = strings.Replace(page, "{Cid}", cid, -1)
	page = strings.Replace(page, "{CopyDate}", c.Date, -1)
	page = strings.Replace(page, "{Copy}", template.HTMLEscapeString(c.Copy), -1)
	fmt.Fprintf(w, page)
}

func getpage(w http.ResponseWriter, r *http.Request) {
	var u UserRequest
	u = setUserRequest(r)
	var cid string
	switch len(u.Args) {
	case 1:
		{ //http://domain
			cid = conf.DefaultLang //home page e.g. domain/en
		}
	default:
		{ //http://domain/blah-blah-blah
			cid = getStr(u.Args, 1)
			if cid == "" {
				cid = conf.DefaultLang
			}
		}
	}
	var p, templateName string
	if misc.IsMobile(r.UserAgent()) {
		templateName = "page4mobile"
	} else {
		templateName = "page"
	}
	var c Copy
	c = readCopy(cid)
	if c.Cid != cid { //cid not found
		p = readSnippet(conf.DefaultLang, templateName)
		p = strings.Replace(p, "{Title}", readSnippet(u.Lang, "Site404Title"), -1)
		p = strings.Replace(p, "<!--Copy-->", readSnippet(u.Lang, "Site404Copy"), -1)
		fmt.Fprintf(w, p)
	} else if c.UsesOwnLayout { //custom design layout
		fmt.Fprintf(w, c.Copy) //c.Copy must contain full page HTML
	} else {
		p = readSnippet(c.Lang, templateName)
		p = strings.Replace(p, "<!--SEOTags-->", c.SEOTags, -1)
		p = strings.Replace(p, "<!--Date-->", c.Date, -1)
		p = strings.Replace(p, "{Title}", c.Title, -1)
		p = strings.Replace(p, "<!--Copy-->", c.Copy, -1)
		fmt.Fprintf(w, p)
	}
	//You can customise getpage to be geolocation-specific by using snippets
	//that correspond to SearchIP result and call it above.
	if conf.ServeHTTPLogging {
		go logHTTPRequest(r) //goroutine here does not help; the page is served already
	}
}

func getblog(w http.ResponseWriter, r *http.Request) {
	var pageNum, lowerBound, upperBound, blogCount int
	var u UserRequest
	blogCount = len(blogIndex)
	if blogCount == 0 {
		http.Redirect(w, r, "/", http.StatusNotFound)
		return
	}
	u = setUserRequest(r)
	//expected URL: domain/blog/ and assumes DefaultLang
	//If you want to separate your blog into langs, then the URL needs to be
	//domain/blog/xx/ or domain/blog/xx/pageNum and use getStr to set u.Lang.
	//
	//Below can also be done with look up by using [][]string in setBlogIndex.
	pageNum, _ = strconv.Atoi(getStr(u.Args, 2)) //domain/blog/page#
	if pageNum < 1 {
		pageNum = 1
	}
	lowerBound = ((pageNum - 1) * conf.BlogsPerPage)
	upperBound = (pageNum * conf.BlogsPerPage) - 1
	if upperBound > (blogCount - 1) { //index is [0 .. blogCount-1]
		upperBound = blogCount - 1
	}
	var blogs, p, templateName, prev, next, blogNav string
	if pageNum > 1 {
		prev = fmt.Sprintf("<a href=\"/blog/%d\">&lt;&lt;</a>", pageNum-1)
	}
	if (pageNum * conf.BlogsPerPage) < blogCount { //[0 .. blogCount-1]
		next = fmt.Sprintf("<a href=\"/blog/%d\">&gt;&gt;</a>", pageNum+1)
	}
	blogNav = readSnippet(conf.DefaultLang, "SiteBlogNav")
	blogNav = strings.Replace(blogNav, "{PrevBlogPage}", prev, -1)
	blogNav = strings.Replace(blogNav, "{NextBlogPage}", next, -1)
	for i := lowerBound; i <= upperBound; i++ {
		cid := blogIndex[i]
		c := readCopy(cid)
		blog := readSnippet(conf.DefaultLang, "SiteBlogList")
		blog = strings.Replace(blog, "{Title}", c.Title, -1)
		blog = strings.Replace(blog, "{Cid}", c.Cid, -1)
		blog = strings.Replace(blog, "<!--Date-->", c.Date, -1)
		blog = strings.Replace(blog, "<!--Copy-->", c.Copy, -1)
		blogs = blogs + blog
	}
	blogs = blogs + blogNav
	if misc.IsMobile(r.UserAgent()) {
		templateName = "page4mobile"
	} else {
		templateName = "page"
	}
	p = readSnippet(conf.DefaultLang, templateName) //or u.Lang, see above
	p = strings.Replace(p, "<!--SEOTags-->", readSnippet(conf.DefaultLang, "SiteBlogSEOTags"), -1)
	p = strings.Replace(p, "{Title}", readSnippet(conf.DefaultLang, "SiteBlogTitle"), -1)
	p = strings.Replace(p, "<!--Copy-->", blogs, -1)
	fmt.Fprintf(w, p)
	if conf.ServeHTTPLogging {
		go logHTTPRequest(r)
	}
}

func getsite(w http.ResponseWriter, r *http.Request) {
	//TODO: del user and del copy
	var u UserRequest
	var session Session
	u, session = getLoginSession(r)
	if session.Id == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	session.resetMsg()
	newCopyLink := fmt.Sprintf("<a href=\"/getcopy/%s/%s\">Add New Copy</a>",
		u.Lang, session.Id)
	var editCopyLinks []string
	for _, c := range copies {
		link := "<li>"
		if c.Date != "" {
			link = link + "Date " + c.Date + ": "
		}
		link = link + fmt.Sprintf(
			"<a href=\"/getcopy/%s/%s/%s\">Edit</a>\n",
			u.Lang, session.Id, c.Cid)
		link = link + fmt.Sprintf(
			"<a href=\"/%s\" target=\"blank\">Preview</a>\n", c.Cid)
		link = link + fmt.Sprintf("(%s) %s / %s</li>\n",
			c.Lang, c.Cid, c.Title)
		editCopyLinks = append(editCopyLinks, link)
	}
	sort.Strings(editCopyLinks)
	var editSnippetLinks []string
	for lang, snips := range snippets {
		for name := range snips {
			if strings.HasPrefix(name, "Site") { // == GUI editable snippets
				link := fmt.Sprintf(
					"<li><a href=\"/getsnippet/%s/%s/%s\">Edit</a> %s (%s)</li>\n",
					u.Lang, session.Id, name, name, lang)
				editSnippetLinks = append(editSnippetLinks, link)
			}
		}
	}
	sort.Strings(editSnippetLinks)
	newUserLink := fmt.Sprintf(
		"<a href=\"/getuser/%s/%s\">Add New User</a>",
		u.Lang, session.Id)
	var editUserLinks []string
	for _, usr := range users {
		link := fmt.Sprintf(
			"<li><a href=\"/getuser/%s/%s/%s\">Edit</a> %s / %s</li>\n",
			u.Lang, session.Id, usr.Email, usr.Email, usr.Name)
		editUserLinks = append(editUserLinks, link)
	}
	sort.Strings(editUserLinks)
	page := readSnippet(u.Lang, "cmsLayout")
	page = strings.Replace(page, "<!--Copy-->", readSnippet(u.Lang, "getsite"), -1)
	page = strings.Replace(page, "<!--cmsNav-->", readSnippet(u.Lang, "cmsNav"), -1)
	page = strings.Replace(page, "<!--editCopyLinks-->", misc.SliceToString(editCopyLinks), -1)
	page = strings.Replace(page, "<!--newCopyLink-->", newCopyLink, -1)
	page = strings.Replace(page, "<!--editSnippetLinks-->", misc.SliceToString(editSnippetLinks), -1)
	page = strings.Replace(page, "<!--editUserLinks-->", misc.SliceToString(editUserLinks), -1)
	page = strings.Replace(page, "<!--newUserLink-->", newUserLink, -1)
	page = strings.Replace(page, "{Lang}", u.Lang, -1)
	page = strings.Replace(page, "{Sid}", session.Id, -1)
	fmt.Fprintf(w, page)
}

func getuser(w http.ResponseWriter, r *http.Request) {
	var u UserRequest
	var session Session
	u, session = getLoginSession(r)
	if session.Id == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	var user User
	if len(u.Args) >= 5 {
		user = loadUser(getStr(u.Args, 4))
	} else {
		user = loadUser("") //new user
	}
	var msg []string
	if session.Last == "getuser" {
		msg = session.LastMsg
	}
	session.Last = "getuser"
	session.save()
	page := readSnippet(u.Lang, "cmsLayout")
	page = strings.Replace(page, "<!--Copy-->", readSnippet(u.Lang, "getuser"), -1)
	page = strings.Replace(page, "<!--cmsNav-->", readSnippet(u.Lang, "cmsNav"), -1)
	page = strings.Replace(page, "<!--Msg-->", readSnippets(u.Lang, msg), -1)
	page = strings.Replace(page, "{Lang}", u.Lang, -1)
	page = strings.Replace(page, "{Sid}", session.Id, -1)
	page = strings.Replace(page, "{Name}", user.Name, -1)
	page = strings.Replace(page, "{Email}", user.Email, -1)
	fmt.Fprintf(w, page)
}

func getLoginSession(r *http.Request) (UserRequest, Session) {
	u := setUserRequest(r)
	u.setLang(getStr(u.Args, 2))
	return u, loadSession(getStr(u.Args, 3))
}

func postuser(w http.ResponseWriter, r *http.Request) {
	var u UserRequest
	var session Session
	u, session = getLoginSession(r)
	if session.Id == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	session.resetMsg()
	var user User
	if len(u.Args) >= 5 {
		email := getStr(u.Args, 4)
		if email != "" {
			user = loadUser(email)
		} else { //assume create new user
			if r.FormValue("repeatemail") != "" &&
				r.FormValue("repeatemail") == r.FormValue("email") {
				user = newUser(r.FormValue("email"))
			} else {
				return
			}
		}
	}
	//CN names >= 2 chars
	if user.Name != r.FormValue("name") && len(r.FormValue("name")) >= 2 {
		user.Name = r.FormValue("name")
	} else if user.Name != r.FormValue("name") &&
		len(r.FormValue("name")) < 2 {
		session.setMsg("ErrNameIsTooShort")
	}
	if r.FormValue("repeatpassword") != "" &&
		r.FormValue("repeatpassword") == r.FormValue("password") {
		user.setPassword(r.FormValue("password"))
	} else {
		if r.FormValue("repeatpassword") != r.FormValue("password") {
			session.setMsg("ErrPasswordMismatch")
		}
	}
	if r.FormValue("repeatemail") != "" &&
		r.FormValue("repeatemail") == r.FormValue("email") &&
		r.FormValue("email") != user.Email {
		newEmail := r.FormValue("email")
		tmpUsr := loadUser(newEmail)
		if tmpUsr.Id != 0 {
			session.setMsg("ErrDuplicateEmail")
		} else {
			tmpUsr = user
			user.Email = newEmail
			session.Email = newEmail
			tmpUsr.del()
		}
	} else if r.FormValue("repeatemail") != "" &&
		r.FormValue("repeatemail") != r.FormValue("email") {
		session.setMsg("ErrEmailMismatch")
	}
	user.save()
	session.save()
	err := backupUsers()
	if err != nil {
		log.Println(err)
	}
	if session.LastMsg != nil {
		http.Redirect(w, r,
			"/getuser/"+u.Lang+"/"+session.Id+"/"+user.Email,
			http.StatusFound)
	} else {
		http.Redirect(w, r,
			"/getsite/"+u.Lang+"/"+session.Id, http.StatusFound)
	}
}

func login(w http.ResponseWriter, r *http.Request) {
	//handles domain/login or domain/login/lang or domain/login/lang/sid
	u := setUserRequest(r)
	if len(u.Args) > 3 { //domain/login/lang/ or domain/login/lang/sid
		u.setLang(getStr(u.Args, 2))
		u.Sid = getStr(u.Args, 3)
	} else if len(u.Args) == 3 { //domain/login/ or domain/login/lang
		u.setLang(getStr(u.Args, 2))
	} else { //domain/login
		u.setLang(conf.DefaultLang)
	}
	email := ""
	var msg []string
	if u.Sid != "" { //to handle redirect from "logged-in" user
		session := loadSession(u.Sid)
		email = session.Email
		//if session.Last == "login" { msg = session.LastMsg }
		msg = session.LastMsg
	}
	page := readSnippet(u.Lang, "cmsLayout")
	page = strings.Replace(page, "<!--Copy-->", readSnippet(u.Lang, "login"), -1)
	page = strings.Replace(page, "<!--Msg-->", readSnippets(u.Lang, msg), -1)
	page = strings.Replace(page, "{Lang}", u.Lang, -1)
	page = strings.Replace(page, "{Sid}", u.Sid, -1)
	page = strings.Replace(page, "{Email}", email, -1)
	fmt.Fprintf(w, page)
}

func postlogin(w http.ResponseWriter, r *http.Request) {
	u := setUserRequest(r)
	u.setLang(getStr(u.Args, 2))
	email := strings.ToLower(strings.TrimSpace(r.FormValue("email")))
	//TODO: must prevent bad format @ client side
	if email == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	session := newSession(email)
	session.Last = "login"
	usr := loadUser(email)
	if usr.Id == 0 {
		session.setMsg("ErrUsrNotFound")
		session.save()
		http.Redirect(w, r,
			"/login/"+u.Lang+"/"+session.Id, http.StatusFound)
		return
	} else { //usr found
		if !usr.passwordEqual(r.FormValue("password")) {
			session.setMsg("ErrWrongPassword")
			session.save()
			http.Redirect(w, r,
				"/login/"+u.Lang+"/"+session.Id, http.StatusFound)
			return
		} else { //login successful
			go delExpiredSessions() //clean up *other* users who did not logout properly
			session.save()
			http.Redirect(w, r,
				"/getsite/"+u.Lang+"/"+session.Id, http.StatusFound)
		}
	}
}

func logout(w http.ResponseWriter, r *http.Request) {
	u := setUserRequest(r)
	u.setLang(getStr(u.Args, 2))
	session := loadSession(getStr(u.Args, 3))
	session.del()
	http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
}

func setSiteMap() { //crude but works
	var s string
	for _, c := range copies {
		s = s + conf.Domain + c.Cid + "\n" //conf.Domain must have trailing /
	}
	fileName := conf.FilePath + "sitemap.txt"
	f, err := os.Create(fileName)
	defer f.Close()
	if err != nil {
		log.Println(err)
	}
	f.Write([]byte(s))
}

func setBlogIndex() { //crude but works
	var idx, tmp []string
	for _, c := range copies {
		if len(c.Date) == 10 { //date format: yyyy-mm-dd; len == 10
			idx = append(idx, c.Date+c.Cid) //date is for sorting only
		}
	}
	blogCount := len(idx)
	if blogCount == 0 {
		return
	}
	sort.Strings(idx)
	for j := blogCount; j > 0; j-- { //remove date and reverse slice
		tmp = append(tmp, idx[j-1][10:])
	}
	blogIndex = tmp
}

func getsnippet(w http.ResponseWriter, r *http.Request) {
	var u UserRequest
	var session Session
	u, session = getLoginSession(r)
	if session.Id == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	var t, tid string
	//domain/getsnippet/lang/sid/ or domain/getsnippet/lang/sid/tid
	if len(u.Args) >= 5 {
		tid = getStr(u.Args, 4)
		if snippetExists(u.Lang, tid) {
			t = readSnippet(u.Lang, tid)
		} else { //domain/getsnippet/lang/sid
			t = ""
		}
	}
	var msg []string
	if session.Last == "getsnippet" {
		msg = session.LastMsg
	}
	page := readSnippet(u.Lang, "cmsLayout")
	page = strings.Replace(page, "<!--Copy-->", readSnippet(u.Lang, "getsnippet"), -1)
	page = strings.Replace(page, "<!--cmsNav-->", readSnippet(u.Lang, "cmsNav"), -1)
	page = strings.Replace(page, "<!--Msg-->", readSnippets(u.Lang, msg), -1)
	page = strings.Replace(page, "{Lang}", u.Lang, -1)
	page = strings.Replace(page, "{Sid}", session.Id, -1)
	page = strings.Replace(page, "{Tid}", tid, -1)
	page = strings.Replace(page, "{Snippet}", template.HTMLEscapeString(t), -1)
	fmt.Fprintf(w, page)
}

func postsnippet(w http.ResponseWriter, r *http.Request) {
	var u UserRequest
	var session Session
	u, session = getLoginSession(r)
	if session.Id == "" {
		http.Redirect(w, r, "/login/"+u.Lang, http.StatusFound)
		return
	}
	session.resetMsg()
	//tid	:= strings.TrimSpace(r.FormValue ("tid"))
	//the above line does not work, maybe bec the HTML field is disabled
	tid := getStr(u.Args, 4)
	snippet := r.FormValue("snippet")
	setSnippet(u.Lang, tid, snippet)
	setPageSnippet(u.Lang)
	err := backupSnippets()
	if err != nil {
		log.Println(err)
	}
	http.Redirect(w, r,
		"/getsite/"+u.Lang+"/"+session.Id, http.StatusFound)
}

func setSnippet(lang, tid, snippet string) {
	snippets[lang][tid] = snippet
}

func setPageSnippet(lang string) {
	page := ""
	page = readSnippet(lang, "SiteLayout")
	page = strings.Replace(page, "<!--SiteNav-->", readSnippet(lang, "SiteNav"), -1)
	page = strings.Replace(page, "<!--SiteHeader-->", readSnippet(lang, "SiteHeader"), -1)
	page = strings.Replace(page, "<!--SiteFooter-->", readSnippet(lang, "SiteFooter"), -1)
	page = strings.Replace(page, "{Lang}", lang, -1) //must be last
	setSnippet(lang, "page", page)
	page4mobile := ""
	page4mobile = readSnippet(lang, "SiteLayout4Mobile")
	page4mobile = strings.Replace(page4mobile, "<!--SiteNav-->", readSnippet(lang, "SiteNav4Mobile"), -1)
	page4mobile = strings.Replace(page4mobile, "<!--SiteHeader-->", readSnippet(lang, "SiteHeader4Mobile"), -1)
	page4mobile = strings.Replace(page4mobile, "<!--SiteFooter-->", readSnippet(lang, "SiteFooter4Mobile"), -1)
	page4mobile = strings.Replace(page4mobile, "{Lang}", lang, -1) //must be last
	setSnippet(lang, "page4mobile", page4mobile)
}

func readSnippet(lang, t string) string {
	if snippetExists(lang, t) {
		return snippets[lang][t]
	}
	return snippets[conf.DefaultLang][t]
}

func readSnippets(lang string, snips []string) (s string) {
	if len(snips) == 0 {
		return ""
	}
	for _, t := range snips {
		s = s + readSnippet(lang, t)
	}
	return s
}

func snippetExists(lang, t string) bool {
	if snippets[lang] == nil || snippets[lang][t] == "" {
		return false
	}
	return true
}

//logHTTPRequest sends HTTP requests to log server set up in startHTTPLogServer.
func logHTTPRequest(r *http.Request) {
	var hlog []string
	hlog = append(hlog, strconv.FormatInt(time.Now().Unix(), 10))
	hlog = append(hlog, r.URL.String())
	hlog = append(hlog, r.RemoteAddr)
	hlog = append(hlog, r.Referer())
	hlog = append(hlog, r.Method)
	hlog = append(hlog, r.UserAgent())
	if conf.TrackLocation {
		hlog = append(hlog, misc.GetIPLocation(r.RemoteAddr, conf.IPCityColumn, conf.IPAreaColumn, conf.IPCountryColumn))
	}
	misc.ToOutboundChannel(hlog)
}

func loadDataFiles() (err error) {
	log.Println("Loading Config") //must be the first to restore
	conf, err = loadConfigFile(confJSON)
	if err != nil {
		log.Println(err)
		return err
	}
	log.Println("Loading MobileRegexp")
	err = misc.SetMobileRegexp(conf.MobileRegexpFile)
	if err != nil {
		log.Println(err)
		return err
	}
	if conf.TrackLocation {
		log.Println("Loading IPLocationTable")
		err = misc.SetIPLocationTable(conf.IPTableFile, conf.IPToColumn)
		if err != nil {
			log.Println(err)
			return err
		}
	}
	if *rFlag == "cms" {
		log.Println("Loading Users")
		tusers := map[string]User{}
		err = misc.ReadJson(conf.UsersFile, &tusers)
		if err != nil {
			log.Println(err)
			return err
		}
		users = tusers
	}
	log.Println("Loading Snippets")
	err = loadSnippetFiles()
	if err != nil {
		log.Println(err)
		return err
	}
	log.Println("Loading Copies") //MUST loadSnippetFiles BEFORE loadCopyFiles
	err = loadCopyFiles()
	if err != nil {
		log.Println(err)
		return err
	}
	return err
}

func loadSnippetFiles() (err error) { //TODO: clean up code
	//folder struct: conf.SnippetPath/lang/tid
	var tsnippets = map[string](map[string]string){}
	f1, e1 := os.Open(conf.SnippetPath)
	defer f1.Close()
	if e1 != nil {
		return e1
	}
	langs, e2 := f1.Readdirnames(-1)
	if e2 != nil {
		return e2
	}
	for _, lang := range langs {
		t := map[string]string{}
		supportedLang[lang] = true
		f2, e3 := os.Open(conf.SnippetPath + lang)
		defer f2.Close()
		if e3 != nil {
			return e3
		}
		snippetNames, e4 := f2.Readdirnames(-1)
		if e4 != nil {
			return e4
		}
		for _, fname := range snippetNames {
			snippetStr, e5 := ioutil.ReadFile(
				conf.SnippetPath + lang + "/" + fname)
			if e5 != nil {
				return e5
			}
			t[fname] = string(snippetStr)
		}
		tsnippets[lang] = t
	}
	snippets = tsnippets
	for _, lang := range langs {
		setPageSnippet(lang)
	}
	return nil
}

func loadCopyFiles() (err error) {
	//folder struct: conf.CopyPath/cid.html and conf.CopyPath/cid.json
	var tcopies = map[string]Copy{}
	f, err := os.Open(conf.CopyPath)
	defer f.Close()
	if err != nil {
		return err
	}
	fnames, err := f.Readdirnames(-1)
	if err != nil {
		return err
	}
	for _, fname := range fnames {
		if strings.HasSuffix(fname, ".json") {
			c := new(Copy)
			err = misc.ReadJson(conf.CopyPath+fname, &c)
			if err != nil {
				return err
			}
			tcopies[c.Cid] = *c
		}
	}
	for cid := range tcopies {
		buf, err := ioutil.ReadFile(conf.CopyPath + cid + ".html")
		if err != nil {
			return err
		}
		c := tcopies[cid] //compiler disliked tcopies [cid].Copy = string (buf)
		c.Copy = string(buf)
		tcopies[cid] = c
	}
	copies = tcopies
	setSiteMap()
	setBlogIndex()
	return nil
}

func backupCopies() (err error) {
	for cid, c := range copies {
		cidpath := conf.CopyPath + cid
		err = ioutil.WriteFile(cidpath+".html", []byte(c.Copy), 0600)
		if err != nil {
			log.Println(err)
			return err
		}
		propOnly := c      //save all metadata to properties.json
		propOnly.Copy = "" //but not the copy markup itself
		err = misc.WriteJson(propOnly, cidpath+".json")
	}
	return nil
}

func backupSnippets() (err error) {
	for lang, snips := range snippets {
		langPath := conf.SnippetPath + lang + "/"
		f1, e1 := os.Open(langPath)
		defer f1.Close()
		if e1 != nil {
			e1 = os.Mkdir(langPath, 0700)
			if e1 != nil {
				return e1
			}
		}
		for name, snippetStr := range snips {
			e1 = ioutil.WriteFile(langPath+name, []byte(snippetStr), 0600)
			if e1 != nil {
				return e1
			}
		}
	}
	return nil
}

func backupUsers() (err error) {
	return misc.WriteJson(users, conf.UsersFile)
}

type User struct {
	Id                       uint32
	Email, Name, PasswordKey string
	PasswordHash             []byte
}

func loadUser(email string) (usr User) {
	if email != "" {
		usr = users[email]
	} else {
		usr = User{} //compiler disliked usr = nil
	}
	return usr
}

func newUser(email string) (usr User) {
	usr.Id = uint32(len(users) + 1)
	usr.Email = email
	users[email] = usr
	return
}

func (usr *User) del() {
	delete(users, usr.Email)
}

func (usr *User) save() {
	users[usr.Email] = *usr
}

func (usr *User) setPassword(newPassword string) {
	usr.PasswordKey, usr.PasswordHash = misc.EncodeString(newPassword)
}

func (usr *User) passwordEqual(password string) bool {
	return misc.EncodingIsOk(password, usr.PasswordKey, usr.PasswordHash)
}

type UserRequest struct {
	Lang, Sid string
	Args      []string
}

func setUserRequest(r *http.Request) (u UserRequest) {
	u.Args = strings.Split(r.URL.Path, "/")
	for i := 0; i < len(u.Args); i++ {
		u.Args[i] = strings.TrimSpace(u.Args[i])
	}
	return u
}

func (u *UserRequest) setLang(s string) {
	u.Lang = strings.ToLower(strings.TrimSpace(s))
	if u.Lang == "" || !supportedLang[u.Lang] {
		u.Lang = conf.DefaultLang
	}
}

func (c *Copy) save() {
	if c.Cid != "" {
		copies[c.Cid] = *c
	}
}

func (c *Copy) del() {
	delete(copies, c.Cid)
}

func (c *Copy) search(str string) (score int) {
	//TODO: fine tune approach for the score multiplier?
	//TODO: index/suffixarray module may search faster but require more memory
	str = strings.ToLower(str)
	score = strings.Count(strings.ToLower(c.Title), str) * 100
	score = score + strings.Count(strings.ToLower(c.Cid), str)*100
	score = score + strings.Count(strings.ToLower(c.Copy), str)*100
	score = score + strings.Count(strings.ToLower(c.SEOTags), str)*50
	words := strings.Fields(str)
	wordCount := len(words)
	if wordCount > 1 { //search term is "white space" separated words
		for i := 0; i < wordCount; i++ {
			score = score + (strings.Count(c.Title, words[i]) * 1)
			score = score + (strings.Count(c.Cid, words[i]) * 1)
			score = score + (strings.Count(c.Copy, words[i]) * 1)
			score = score + (strings.Count(c.SEOTags, words[i]) * 1)
		}
	}
	return score
}

type Session struct {
	Id, Email, Last string //e.g. session.Last = "login"
	LastMsg         []string
	ExpireTime      int64
}

func (session *Session) del() {
	if session.Id != "" {
		delete(sessions, session.Id)
	}
}

func (session *Session) save() {
	if session.Id != "" {
		session.setExpiry()
		sessions[session.Id] = *session
	}
}

func (session *Session) setExpiry() {
	if session.Id != "" {
		session.ExpireTime = time.Now().Unix() + 1800 //1800 seconds = 30 minutes
	}
}

func (session *Session) setMsg(msg string) {
	if session.Id != "" {
		session.LastMsg = append(session.LastMsg, msg)
	}
}

func (session *Session) resetMsg() {
	if session.Id != "" {
		session.LastMsg = nil
	}
}

func loadSession(sid string) Session {
	if sid == "" {
		return Session{}
	}
	for id, session := range sessions {
		if id == sid {
			if session.ExpireTime >= time.Now().Unix() {
				session.setExpiry() //renew expiry
				session.save()
				return session
			} else {
				return Session{}
			}
		}
	}
	return Session{}
}

func newSession(email string) (session Session) {
	id := strconv.FormatInt(time.Now().Unix(), 10) + "-" + email
	id = base64.URLEncoding.EncodeToString([]byte(id))
	session = Session{Id: id, Email: email}
	session.save()
	return session
}

func delExpiredSessions() {
	for _, session := range sessions {
		if session.ExpireTime < time.Now().Unix() {
			session.del()
		}
	}
}

type Configuration struct {
	DefaultLang, HTTPServerPort, FilePath, UsersFile, CopyPath, SnippetPath,
	LogPath, RPCConnType, RPCConnAddr, TLSServerPort, TLSCertFile,
	TLSKeyFile, LogChannelName, LogChannelType, LogChannelAddr, Domain,
	MobileRegexpFile, IPTableFile string
	ServeTLS, ServeRPC, ServeHTTPLogging, TrackLocation     bool
	LogChannelBufSize, BlogsPerPage                         int
	IPToColumn, IPCityColumn, IPAreaColumn, IPCountryColumn int
}

func loadConfigFile(fname string) (c Configuration, err error) {
	tconfig := Configuration{}
	err = misc.ReadJson(fname, &tconfig)
	if err != nil {
		log.Println(err)
		return Configuration{}, err
	}
	c = tconfig
	return c, nil
}

type Copy struct {
	Cid, Copy, SEOTags, Title, Lang, Date string
	Tags                                  []string //TODO: tags (for blog)
	UsesOwnLayout                         bool     //ignore site layout: Copy str == full page static html
}

func readCopy(cid string) Copy {
	if cid == "" {
		return Copy{}
	}
	return copies[cid]
}

func (c *Copy) cidExists(s string) bool {
	if copies[s].Cid != "" {
		return true
	}
	return false
}

type Admin struct{}

//Do is an RPC for exposed func.  If not exported, it causes runtime err during
//'rpc register'.
func (r *Admin) Do(in, out *string) (err error) {
	switch *in {
	case "update":
		return loadDataFiles()
	}
	return nil
}

//getStr is a convenience & safety wrapper to retrieve an item from a slice
func getStr(slice []string, idx int) string {
	if len(slice) > idx {
		return slice[idx]
	}
	return ""
}

//Copyright: Slamet Hendry
//Thank you:
//Rick Morrison updated lib usage from r60 to weekly.2012-02-22
//	- strconv: Itoa64 to FormatInt
//	- time: Seconds to Now().Unix()
//	- http.Request: RawURL to URL.Path & URL.String()
//	- runtime: MemStats to ReadMemStats (later I dropped this)
//	- os.Error to error
